﻿/*
<File>
	<Copyright>Copyright © 2007, Daniel Vaughan. All rights reserved.</Copyright>
	<License see="//License.txt"/>
	<Owner Name="Daniel Vaughan" Email="danielvaughan@outcoder.com"/>
	<CreationDate>2007/11/19 20:00</CreationDate>
	<LastSubmissionDate>$Date: $</LastSubmissionDate>
	<Version>$Revision: $</Version>
</File>
*/

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.ServiceModel;
using System.Threading;
using System.Windows;
using System.Windows.Browser;

using DanielVaughan.Logging.ClientLogging;
using DanielVaughan.Logging.Configuration;
using DanielVaughan.ServiceModel;

namespace DanielVaughan.Logging
{
	/// <summary>
	/// The default implementation of an <see cref="ILog"/>.
	/// </summary>
	partial class Log : ILog
	{
		readonly Type hostType;
		readonly string name;
		ClientConfigurationData clientConfigurationData;
		readonly object loggingConfigLock = new object();
		static Uri pageUriUseProperty;

		static Uri PageUri
		{
			get
			{
				if (pageUriUseProperty == null)
				{
					if (Application.Current.IsRunningOutOfBrowser)
					{
						pageUriUseProperty = new Uri(Application.Current.Host.Source.AbsoluteUri, UriKind.Absolute);
					}
					else
					{
						pageUriUseProperty = HtmlPage.Document.DocumentUri;
					}
				}
				return pageUriUseProperty;
			}
		}

		#region event InternalMessage

		event EventHandler<InternalMessageEventArgs> internalMessage;

		public event EventHandler<InternalMessageEventArgs> InternalMessage
		{
			add
			{
				internalMessage += value;
			}
			remove
			{
				internalMessage -= value;
			}
		}

		protected void OnInternalMessage(InternalMessageEventArgs e)
		{
			if (internalMessage != null)
			{
				internalMessage(this, e);
			}
		}

		#endregion

		public string Name
		{
			get
			{
				if (hostType != null)
				{
					return GetName(hostType);
				}
				return name;
			}
		}

		/// <summary>
		/// Gets the name for the log using the specified hostType.
		/// <seealso cref="Log.GetUrlForLogName"/>
		/// </summary>
		/// <param name="hostType">Type of the host where the log is located.</param>
		/// <returns>The name of the log.</returns>
		public static string GetName(Type hostType)
		{
			return string.Format("{0},{1}", hostType.FullName, GetUrlForLogName());
		}

		/// <summary>
		/// Gets the name of the URL for the log data.
		/// If we are logging from localhost then we do 
		/// not use the port number, because it is assumed
		/// that we are debugging.
		/// </summary>
		/// <returns>The url of the page without the querystring.</returns>
		static string GetUrlForLogName()
		{
			string result;
			Uri uri = PageUri;
			if (uri.Host.ToLower() == "localhost" || uri.Host.Contains("127.0.0."))
			{
				result = string.Format("{0}", uri.LocalPath);
			}
			else
			{
				if (uri.Port == 80 || uri.Port == 443)
				{
					result = string.Format("{0}://{1}{2}", uri.Scheme, uri.Host, uri.LocalPath);
				}
				else
				{
					result = string.Format("{0}://{1}:{2}{3}", uri.Scheme, uri.Host, uri.Port, uri.LocalPath);
				}
			}
			
			return result.ToLower();				
		}

		#region event LogEntrySent
		event EventHandler<LogEventArgs> logEntrySent;

		public event EventHandler<LogEventArgs> LogEntrySent
		{
			add
			{
				logEntrySent += value;
			}
			remove
			{
				logEntrySent -= value;
			}
		}

		protected void OnLogEntrySent(LogEventArgs e)
		{
			if (logEntrySent != null)
			{
				logEntrySent(this, e);
			}
		}
		#endregion

		#region event WriteRequested
		event EventHandler<LogEventArgs> logEntrySendAttempt;

		public event EventHandler<LogEventArgs> WriteRequested
		{
			add
			{
				logEntrySendAttempt += value;
			}
			remove
			{
				logEntrySendAttempt -= value;
			}
		}

		protected void OnLogEntrySendAttempt(LogEventArgs e)
		{
			if (logEntrySendAttempt != null)
			{
				logEntrySendAttempt(this, e);
			}
		}
		#endregion

		public Log(Type hostType)
		{
			this.hostType = hostType;
		}

		public Log(string name)
		{
			this.name = name;
		}

		/// <summary>
		/// Determines whether logging is enabled for the specified log level.
		/// </summary>
		/// <param name="logLevel">The log level.</param>
		/// <returns>
		/// 	<c>true</c> if logging is enabled for the specified level; otherwise, <c>false</c>.
		/// </returns>
		bool IsLevelEnabled(LogLevel logLevel)
		{
			try
			{
				if (clientConfigurationData == null) /* Nice to have ReaderWriterSlimLock in Silverlight. */
				{
					return true; /* ConfigurationData will be assigned on log attempt. We can improve performance here. */
				}
				return logLevel >= clientConfigurationData.LogLevel;
			}
			catch (Exception ex)
			{
				string errorMessage = string.Format("Unable to determine enabled level."); /* TODO: Make localizable resource. */
				OnInternalMessage(new InternalMessageEventArgs(errorMessage, ex));
			}
			return false;
		}

		bool clogServiceConfigFailed;
		static EndpointAddress fallBackEndpoint;

		IClogService GetClogService()
		{
			if (!clogServiceConfigFailed)
			{
				try
				{
					var clogService = ChannelManagerSingleton.Instance.GetChannel<IClogService>();
					return clogService;
				}
				catch (Exception ex)
				{
					clogServiceConfigFailed = true;
					OnInternalMessage(
						new InternalMessageEventArgs(
							"Unable to use WCF config for channel. Using default configuration. This may be acceptable if no end point has been defined.",
							ex));
				}
			}
            
			if (fallBackEndpoint == null)
			{
				Uri uri = PageUri;
				string cleanURL = uri.Scheme + "://" + uri.GetComponents(UriComponents.HostAndPort, UriFormat.UriEscaped);
				var serviceUri = new Uri(cleanURL + "/ClogService.svc");
				fallBackEndpoint = new EndpointAddress(serviceUri);
			}
			var result = new ClogServiceClient(new ClogBasicHttpBinding(), fallBackEndpoint);
            
			return result;
		}

		void WriteLogEntry(LogLevel logLevel, string message, Exception exception, IDictionary<string, object> properties)
		{
			try
			{
				WriteLogEntryAux(logLevel, message, exception, properties);
			}
			catch (Exception ex)
			{
				string errorMessage = string.Format("Unable to write to log: {0} {1}{2}",
				                                    ex, Environment.NewLine, ex.StackTrace);
				Console.WriteLine(errorMessage);
				OnInternalMessage(new InternalMessageEventArgs(errorMessage, ex));
			}
		}

		void WriteLogEntryAux(LogLevel logLevel, string message, 
			Exception exception, IDictionary<string, object> properties)
		{
			ExceptionMemento memento = null;
			if (exception != null)
			{
				memento = CreateMemento(exception);
			}

			var logEntryData = new LogEntryData
			            	{
			            		LogLevel = logLevel,
			            		Message = message,
			            		ExceptionMemento = memento,
			            		CodeLocation = GetLocation(),
			            		LogName = Name,
			            		ThreadName = Thread.CurrentThread.Name,
			            		ManagedThreadId = Thread.CurrentThread.ManagedThreadId,
			            		Url = PageUri.ToString(),
			            		OccuredAt = DateTime.Now,
			            		Properties = properties != null ? new Dictionary<string, object>(properties) : null
			            	};

			OnLogEntrySendAttempt(new LogEventArgs(logEntryData));

			var clientInfo = new ClientInfo
			           	{
			           		LogName = logEntryData.LogName,
			           		MachineName = logEntryData.MachineName,
			           		Url = logEntryData.Url,
			           		UserName = logEntryData.UserName
			           	};


			if (clientConfigurationData == null 
				|| clientConfigurationData.RetrievedOn.AddSeconds(clientConfigurationData.ExpiresInSeconds) < DateTime.Now)
			{
				lock (loggingConfigLock)
				{
					if (clientConfigurationData == null 
						|| clientConfigurationData.RetrievedOn.AddSeconds(clientConfigurationData.ExpiresInSeconds) < DateTime.Now)
					{					
						var clogService = GetClogService();
						clogService.BeginGetConfiguration(clientInfo,
							asyncResult =>
							{
								ClientConfigurationData result;
								try
								{
									result = clogService.EndGetConfiguration(asyncResult);
								}
								catch (Exception ex)
								{
									OnInternalMessage(
										new InternalMessageEventArgs("Unable to retrieve configuration from server.", ex));
									return;
								}

								try
								{
									clientConfigurationData = result;
									clientConfigurationData.RetrievedOn = DateTime.Now;
									WriteLogEntryAux(logEntryData, clientConfigurationData);
								}
								catch (Exception ex)
								{
									OnInternalMessage(
										new InternalMessageEventArgs(
											"Problem writing log entry after successfully retrieving configuration.", /* TODO: Make localizable resource. */
											ex));
									throw;
								}

							}, null);					
					    
                        return;
					}
				}
			}
            WriteLogEntryAux(logEntryData, clientConfigurationData);
		}

        void WriteLogEntryAux(LogEntryData entry, ClientConfigurationData config)
        {
    		string notLoggingReason = string.Empty;
			if (config == null)
			{
				notLoggingReason = "Server config is null"; /* TODO: Make localizable resource. */
			}
			else if (!config.LogEnabled)
			{
				notLoggingReason = "Log not enabled on server. Check filters if this is unexpected."; /* TODO: Make localizable resource. */
			}
			else if (entry.LogLevel < config.LogLevel)
			{
				notLoggingReason = "Log level to low below threshold."; /* TODO: Make localizable resource. */
			}

			if (notLoggingReason != string.Empty)
			{
				string message = string.Format("Not sending message to server. Reason: {0}", notLoggingReason); /* TODO: Make localizable resource. */
				OnInternalMessage(new InternalMessageEventArgs(message));
				return;
			}

        	/* Send of the log entry to the web service. */
			try
			{
				var clogService = GetClogService();
				clogService.BeginWriteEntrySilverlight(entry, 
					ar => 
						{
                    		try
                    		{
								clogService.EndWriteEntrySilverlight(ar);
                    		}
                    		catch (Exception ex)
                    		{
								OnInternalMessage(new InternalMessageEventArgs("Unable to send log entry", ex)); /* TODO: Make localizable resource. */
                    			return;
                    		}
							try
							{
								OnLogEntrySent(new LogEventArgs(entry));
							}
							catch (Exception ex)
							{
								OnInternalMessage(new InternalMessageEventArgs("Problem raising event via OnLogEntrySent", ex)); /* TODO: Make localizable resource. */
								return;
							}
							
                    	}, null);
			}
			catch (Exception ex)
			{
				OnInternalMessage(new InternalMessageEventArgs("Unable to send log entry", ex)); /* TODO: Make localizable resource. */

				return;
			}
        }
        
		static ExceptionMemento CreateMemento(Exception exception)
		{
			var memento = new ExceptionMemento
				{
					TypeName = exception.GetType().AssemblyQualifiedName,
					Message = exception.Message,
					Source = string.Empty,//exception.Source, No Source property in Silverlight BCL.
					StackTrace = exception.StackTrace ?? string.Empty,
					HelpLink = string.Empty
				};
			return memento;
		}

		CodeLocation GetLocation()
		{
			//return GetCallerLocation();
			var codeLocation = new CodeLocation
				{
					ClassName = hostType != null ? hostType.AssemblyQualifiedName : string.Empty,
					MethodName = string.Empty,
					FileName = string.Empty,
					LineNumber = -1
				};
			return codeLocation;
		}

		/// <summary>
		/// Gets the caller location.
		/// Throws MethodAccessException. 
		/// We can't do this in Silverlight, so don't use it.
		/// Left here for your amusement.
		/// </summary>
		/// <returns>The source code location that the call 
		/// to log was made.</returns>
		static CodeLocation GetCallerLocation()
		{
			StackTrace trace;
			string className;
			string methodName;
			string fileName;
			int lineNumber;
			StackFrame frame = null;
			
			try
			{
				trace = new StackTrace(new Exception());
				frame = trace.GetFrame(2);
			}
			catch (MethodAccessException ex)
			{
				string stackTrace = ex.StackTrace;
			}

			if (frame != null)
			{
				className = frame.GetMethod().ReflectedType.FullName;
				methodName = frame.GetMethod().Name;
				//fileName = frame.GetFileName();
				lineNumber = frame.GetFileLineNumber();
			}
			else
			{
				className = string.Empty;
				methodName = string.Empty;
				fileName = string.Empty;
				lineNumber = 0;
			}

			var location = new CodeLocation
			{
				ClassName = className,
				MethodName = methodName,
				//FileName = fileName,
				LineNumber = lineNumber
			};
			return location;
		}
	}
}
